/*
 * Copyright 2008-2010 Daniel Cachapa <cachapa@gmail.com>
 * 
 * This program is distributed under the terms of the GNU General Public License Version 3
 * The license can be read in its entirety in the LICENSE.txt file accompanying this source code,
 * or at: http://www.gnu.org/copyleft/gpl.html
 * 
 * This file is part of Libra.
 *
 * WeightWatch is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * WeightWatch is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with the WeightWatch source code. If not, see: http://www.gnu.org/licenses
 */

package net.cachapa.libra.chart;

import java.util.Vector;

import net.cachapa.libra.Preferences;
import net.cachapa.libra.R;
import net.cachapa.libra.data.Database;
import net.cachapa.libra.data.Value;
import net.cachapa.libra.util.Bmi;
import net.cachapa.libra.util.MyDate;
import net.cachapa.libra.util.UnitManager;

import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.CornerPathEffect;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Paint.Align;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Style;
import android.preference.PreferenceManager;

public class Chart {
	private static final int AXIS_COLOUR = 0xffeeeeee;
	private static final int AXIS_TEXT_COLOUR = 0xffffffff;
	private static final float AXIS_WIDTH = 1;
	private static final int TICK_COLOUR = 0xff999999;
	private static final int TICK_SIZE = 3;
	private static final int GUIDE_COLOUR = 0x66aaaaaa;
	private static final int LIGHT_GUIDE_COLOUR = 0x66666666;
	private static final int LIGHT_BACKGROUND_COLOUR = 0x22999999;
	private static final int DARK_BACKGROUND_COLOUR = 0x00666666;
	private static final int FLOATER_LINE_COLOUR = 0xaaffffff;
	private static final int FLOATER_LINE_WIDTH = 2;
	private static final int FLOATER_SIZE = 2;
	private static final int FLOATER_COLOUR = 0xffeeeeee;
	private static final int FLOATER_FILL_COLOUR = 0xffff3300;
	private static final int TREND_COLOUR = 0xffff3300;
	private static final float TREND_WIDTH = 2.3f;
	private static final int GOAL_LINE_COLOUR = 0xff0033dd;
	private static final int GOAL_TEXT_COLOUR = 0xff0088ee;
	private static final int BMI_LINE_COLOUR = 0xffff9900;
	private static final int BMI_TEXT_COLOUR = 0xffffcc00;
	private static final int TEXT_SIZE = 15;
	
	private int stringPadding = 5;
	private float minMaxLabelPadding = 0.2f;
	
	private int width, height;
	private int textWidth, textHeight;
	private int topPadding, bottomPadding, leftPadding, rightPadding;
	private float minValue, maxValue;
	private MyDate startDate, endDate;
	private Vector<Value> values;
	private Bitmap image;
	private Canvas canvas;
	private Paint axisPaint, xAxisTextPaint, yAxisTextPaint, tickPaint, expectedPaint, guidePaint, lightGuidePaint, lightBackgroundPaint,
		darkBackgroundPaint, floaterPaint, floaterFillPaint, floaterLinePaint, trendPaint, goalLinePaint, goalTextPaint,
		bmiLinePaint, bmiTextPaint;
	private float horizontalStep;
	private float verticalStep;
	private float scale;
	private Context context;
	
	public Bitmap generateChart(int width, int height, Vector<Value> values, MyDate startDate, MyDate endDate, Context context, float scale) {
		this.width = width;
		this.height = height;
		this.values = values;
		this.startDate = startDate;
		this.endDate = endDate;
		this.context = context;
		this.scale = scale;
		
		stringPadding *= scale;
		minMaxLabelPadding *= scale;
		
		image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
		canvas = new Canvas(image);
		
		axisPaint = new Paint();
		axisPaint.setColor(AXIS_COLOUR);
		axisPaint.setStrokeWidth(AXIS_WIDTH * scale);
		axisPaint.setStyle(Style.STROKE);
		axisPaint.setStrokeCap(Cap.ROUND);
		
		xAxisTextPaint = new Paint();
		xAxisTextPaint.setAntiAlias(true);
		xAxisTextPaint.setColor(AXIS_TEXT_COLOUR);
		xAxisTextPaint.setTextAlign(Align.CENTER);
		xAxisTextPaint.setTextSize(TEXT_SIZE * scale);
		
		yAxisTextPaint = new Paint();
		yAxisTextPaint.setAntiAlias(true);
		yAxisTextPaint.setColor(AXIS_TEXT_COLOUR);
		yAxisTextPaint.setTextAlign(Align.RIGHT);
		yAxisTextPaint.setTextSize(TEXT_SIZE * scale);
		
		tickPaint = new Paint();
		tickPaint.setColor(TICK_COLOUR);
		tickPaint.setStyle(Style.STROKE);
		
		guidePaint = new Paint();
		guidePaint.setColor(GUIDE_COLOUR);
		
		lightGuidePaint = new Paint();
		lightGuidePaint.setColor(LIGHT_GUIDE_COLOUR);
		
		lightBackgroundPaint = new Paint();
		lightBackgroundPaint.setColor(LIGHT_BACKGROUND_COLOUR);
		
		darkBackgroundPaint = new Paint();
		darkBackgroundPaint.setColor(DARK_BACKGROUND_COLOUR);
		
		floaterPaint = new Paint();
		floaterPaint.setAntiAlias(true);
		floaterPaint.setColor(FLOATER_COLOUR);
		floaterPaint.setStrokeWidth(1.5f * scale);
		floaterPaint.setStyle(Style.STROKE);
		
		floaterFillPaint = new Paint();
		floaterFillPaint.setAntiAlias(true);
		floaterFillPaint.setColor(FLOATER_FILL_COLOUR);
		floaterFillPaint.setStrokeWidth(1.5f * scale);
		floaterFillPaint.setStyle(Style.FILL);
		
		floaterLinePaint = new Paint();
		floaterLinePaint.setAntiAlias(true);
		floaterLinePaint.setColor(FLOATER_LINE_COLOUR);
		floaterLinePaint.setStrokeWidth(FLOATER_LINE_WIDTH * scale);
		
		trendPaint = new Paint();
		trendPaint.setAntiAlias(true);
		trendPaint.setStrokeWidth(TREND_WIDTH * scale);
		trendPaint.setStrokeCap(Cap.ROUND);
		trendPaint.setColor(TREND_COLOUR);
		trendPaint.setStyle(Paint.Style.STROKE);
		trendPaint.setPathEffect(new CornerPathEffect(10 * scale));
		
		expectedPaint = new Paint();
		expectedPaint.setAntiAlias(true);
		expectedPaint.setStrokeWidth(TREND_WIDTH * scale);
		expectedPaint.setColor(TREND_COLOUR);
		expectedPaint.setStyle(Paint.Style.STROKE);
		expectedPaint.setPathEffect(new DashPathEffect(new float[]{5 * scale,5 * scale}, 1 * scale));
		
		goalLinePaint = new Paint();
		goalLinePaint.setColor(GOAL_LINE_COLOUR);
		
		goalTextPaint = new Paint();
		goalTextPaint.setColor(GOAL_TEXT_COLOUR);
		goalTextPaint.setAntiAlias(true);
		goalTextPaint.setTextSkewX(-0.25f);
		goalTextPaint.setFakeBoldText(true);
		goalTextPaint.setTextAlign(Align.RIGHT);
		goalTextPaint.setTextSize(TEXT_SIZE * scale);
		
		bmiLinePaint = new Paint();
		bmiLinePaint.setColor(BMI_LINE_COLOUR);
		bmiLinePaint.setPathEffect(new DashPathEffect(new float[]{5 * scale,5 * scale}, 1 * scale));
		
		bmiTextPaint = new Paint();
		bmiTextPaint.setColor(BMI_TEXT_COLOUR);
		bmiTextPaint.setAntiAlias(true);
		bmiTextPaint.setTextSkewX(-0.25f);
		bmiTextPaint.setTextAlign(Align.RIGHT);
		bmiTextPaint.setTextSize(TEXT_SIZE * scale);
		
		determineMinMax();
		computePaddings();
		
		drawAxis();
		
		SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
		float goalWeight = prefs.getFloat("goalWeightPreference", 0);
		if (goalWeight > 0) {
			drawGoal(goalWeight);
		}
		
		float userHeight = prefs.getFloat("heightPreference", 0);
		boolean showBmiLines = prefs.getBoolean(Preferences.SHOW_BMI_LINES, true);
		if (userHeight > 0 && showBmiLines) {
			drawBmiLines(userHeight);
		}
		
		drawChart();
		
		return image;
	}

	private void drawAxis() {
		drawXAxis();
		drawYAxis();
		Path axis = new Path();
		axis.moveTo(leftPadding, 1);
		axis.lineTo(leftPadding, bottomPadding);
		axis.lineTo(width-2, bottomPadding);
		canvas.drawPath(axis, axisPaint);
		// Now block anyone from drawing over the main axis
		canvas.clipRect(leftPadding + 1, 0, width, bottomPadding - 1);
	}

	private void drawXAxis() {
		int days = startDate.differenceInDays(endDate) + 1;
		horizontalStep = (width - leftPadding - rightPadding) / (float) days;
		int labelHalfWidth;
//		boolean showWeekdays = days <= 7;
		boolean showDays = days < 60;
//		boolean showMonths = startDate.getMonth() != endDate.getMonth();
//		boolean showYears = startDate.getYear() != endDate.getYear();
//		if (showYears) {
//			labelHalfWidth = (int)xAxisTextPaint.measureText("0000") / 2;
//		}
//		else if (showMonths) {
//			labelHalfWidth = (int)xAxisTextPaint.measureText("mmm") / 2;
//		}
//		else if (showWeekdays) {
//			labelHalfWidth = (int)xAxisTextPaint.measureText("mmm") / 2;
//		}
//		else {
//			labelHalfWidth = (int)xAxisTextPaint.measureText("00") / 2;
//		}
		if (showDays) {
			labelHalfWidth = (int)xAxisTextPaint.measureText("00") / 2;
		}
		else {
			labelHalfWidth = (int)xAxisTextPaint.measureText("mmm") / 2;
		}
		String label;
		int lastLabel = 0;
		int startDay = startDate.getDay();
		int startWeekday = startDate.getDayOfWeek();
		int firstDayOfWeek = startDate.getFirstDayOfWeek();
		int startMonth = startDate.getMonth();
		int startYear = startDate.getYear();
		float lastSum = leftPadding;
		Paint backgroundPaint = darkBackgroundPaint;
		float sum = leftPadding + horizontalStep;
		int y = bottomPadding + textHeight + stringPadding;
		int currentDay;
		for (int i = 1; i <= days; i++) {
			currentDay = startDay + i - 1;
			if (showDays) {
				if (currentDay >= startDate.getDaysInMonth()) {
					startDay -= startDate.getDaysInMonth();
				}
				if (sum - labelHalfWidth > lastLabel + stringPadding && sum + labelHalfWidth < width - stringPadding) {
					label = String.valueOf(currentDay);
					canvas.drawText(label, sum, y, xAxisTextPaint);
					lastLabel = (int)(sum + labelHalfWidth);
				}
				if (((startWeekday + i - 1) % 7) == firstDayOfWeek) {
					// Draw the background
					canvas.drawRect(lastSum, 0, sum, bottomPadding, backgroundPaint);
					lastSum = sum;
					// Switch the background colours
					if (backgroundPaint == lightBackgroundPaint) {
						backgroundPaint = darkBackgroundPaint;
					}
					else {
						backgroundPaint = lightBackgroundPaint;
					}
				}
				canvas.drawLine((int)sum, bottomPadding, (int)sum, bottomPadding - TICK_SIZE, tickPaint);
			}
			else {
				if (currentDay > MyDate.getDaysInMonth(startMonth, startYear)) {
					// Advance a month
					startDay -= MyDate.getDaysInMonth(startMonth, startYear);
					startMonth++;
					if (startMonth > 12) {
						startMonth = 1;
						startYear++;
					}
				}
				if (startDay + i - 1 == 1) {
					label = String.valueOf(MyDate.getShortMonthName(startMonth));
//					labelHalfWidth = axisTextPaint.measureText(label) / 2;
					if (sum - labelHalfWidth > lastLabel + stringPadding) {
						canvas.drawText(label, sum, y, xAxisTextPaint);
						lastLabel = (int)(sum + labelHalfWidth);
					}
					// Draw the background
					canvas.drawRect(lastSum, 0, sum, bottomPadding, backgroundPaint);
					lastSum = sum;
					// Switch the background colours
					if (backgroundPaint == lightBackgroundPaint) {
						backgroundPaint = darkBackgroundPaint;
					}
					else {
						backgroundPaint = lightBackgroundPaint;
					}
				}
			}
			
			sum += horizontalStep;
		}
		// Draw the last piece of the background
		canvas.drawRect(lastSum, 0, sum, bottomPadding, backgroundPaint);
	}
	
	private void drawYAxis() {
		double labelHalfHeight = textHeight / 2;
		verticalStep = (bottomPadding - topPadding) / (float) (maxValue - minValue);
		boolean isStones = UnitManager.getInstance(context).isStones();
		
		// Determines if we're going to show all labels, and if not, how many to jump each time
		int labelJump = 1;
		while (verticalStep * labelJump < textHeight + stringPadding) {
			labelJump++;
		}
		
		String label;
		double sum = bottomPadding;
		for (int i = 0; i <= (maxValue - minValue) + 1; i++) {
			canvas.drawLine(leftPadding, (int)sum, width, (int)sum, guidePaint);
			if (i % labelJump == 0 && sum - labelHalfHeight > 0) {
				if (isStones) {
					label = (int)((minValue + i) / 14) + "s" + (int)((minValue + i) % 14);
				}
				else {
					label = String.valueOf((int)minValue + i);
				}
				canvas.drawText(label, leftPadding - stringPadding, (int)(sum + labelHalfHeight), yAxisTextPaint);
			}
			if (verticalStep > 40) {		// Draw lines for x.5 when the vertical space allows
				canvas.drawLine(leftPadding, (int)(sum - verticalStep / 2), width, (int)(sum - verticalStep / 2), lightGuidePaint);
			}
			sum -= verticalStep;
		}
	}
	
	private void drawChart() {
		Path trendPath = new Path();
		Value value = null;
		float x = 0, yWeight, yTrend = 0;
		int size = values.size();
		if (size == 0) {
			return;
		}
		float radius = FLOATER_SIZE * scale;
		float radiusFill = (FLOATER_SIZE - 0.3f) * scale;
		if (horizontalStep < 2) {
			floaterLinePaint.setStrokeWidth(1);
			floaterPaint.setStrokeWidth(1);
			radius = radiusFill = horizontalStep;
			trendPaint.setStrokeWidth(2);
			expectedPaint.setStrokeWidth(2);
		}
		for (int i = 0; i < size; i++) {
			value = (Value)values.elementAt(i);
			x = xCoord(startDate.differenceInDays(value.getDate()));
			yWeight = yCoord(value.getWeight());
			yTrend = yCoord(value.getTrend());
			// Sinks and floaters
			if (!value.getDate().isBefore(startDate)) {		// Avoids drawing floaters from the previous month
				canvas.drawLine(x, yWeight, x, yTrend, floaterLinePaint);
				canvas.drawCircle(x, yWeight, radius, floaterPaint);
				canvas.drawCircle(x, yWeight, radiusFill, floaterFillPaint);
			}
			// Trend line
			if (i == 0) {
				trendPath.moveTo(x, yTrend);
			}
			trendPath.lineTo(x, yTrend);
		}
		
		// Expected line
		SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
		if (prefs.getBoolean(Preferences.SHOW_EXPECTED_LINE, true)) {
			Value goalValue = Database.getInstance(context).getGoalValue();
			if (value.getDate().isBefore(endDate) && goalValue != null) {
				int days;
				// This fixes a bug where if the expected date is too far in the future, then the line wouldn't draw
				if ((goalValue.getDate().getYear() - value.getDate().getYear()) > 5) {
					goalValue.setDate(new MyDate(value.getDate().getYear(), 12, 31));
					days = startDate.differenceInDays(goalValue.getDate());
					// Some trigonometry...
					float newTrend = value.getTrend() + (goalValue.getTrend() - value.getTrend()) / days;
					goalValue.setTrend(newTrend);
				}
				else {
					days = startDate.differenceInDays(goalValue.getDate());
				}
				float expectedX = xCoord(days);
				float expectedY = yCoord(goalValue.getTrend());
				canvas.drawLine(x, yTrend, expectedX, expectedY, expectedPaint);
			}
		}
		
		trendPath.lineTo(x, yTrend);
		canvas.drawPath(trendPath, trendPaint);
		canvas.drawLine(x, yTrend, x, yTrend, trendPaint);
	}
	
	private void drawGoal(float goalWeight) {
		float y = yCoord(goalWeight);
		if (y < 0 || y > bottomPadding) {
			return;
		}
		canvas.drawLine(leftPadding, y, width, y, goalLinePaint);
		String goalLabel = context.getString(R.string.goal);
		int x = width - stringPadding;
		canvas.drawText(goalLabel, x, y - 2, goalTextPaint);
	}
	
	private void drawBmiLines(float userHeight) {
			drawBmiLine(Bmi.SEVERELY_UNDERWEIGHT, userHeight);
			drawBmiLine(Bmi.UNDERWEIGHT, userHeight);
			drawBmiLine(Bmi.NORMAL, userHeight);
			drawBmiLine(Bmi.OVERWEIGHT, userHeight);
			drawBmiLine(Bmi.OBESE_I, userHeight);
			drawBmiLine(Bmi.OBESE_II, userHeight);
			drawBmiLine(Bmi.OBESE_III, userHeight);
	}
	
	private void drawBmiLine(double bmi, float userHeight) {
		UnitManager um = UnitManager.getInstance(context);
		float bmiLine = Bmi.getWeight(bmi, userHeight, um.isMetricHeight(), um.isMetricWeight());
		float y = yCoord(bmiLine);
		if (y < bmiTextPaint.ascent() || y > bottomPadding) {
			return;
		}
		String levelName = Bmi.getLevelName(bmi, context);
		int x = width - stringPadding;
		canvas.drawText(levelName, x, y - 4, bmiTextPaint);
		canvas.drawLine(leftPadding, y, width, y, bmiLinePaint);
		levelName = Bmi.getLevelName(bmi-1, context);
		canvas.drawText(levelName, x, y  - bmiTextPaint.ascent() + 1, bmiTextPaint);
	}
	
	private void determineMinMax() {
		SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
		float goal = prefs.getFloat(Preferences.GOAL_WEIGHT, 0);
		
		if (values.isEmpty()) {
			if (goal == 0) {
				minValue = 77;
				maxValue = 80;
			}
			else {
				minValue = maxValue = goal;
			}
		}
		else {
			Value value = (Value)values.firstElement();
			minValue = value.getWeight();
			maxValue = value.getWeight();
			
			if (prefs.getBoolean(Preferences.SHOW_GOAL, true) && goal > 0) {
				minValue = Math.min(minValue, goal);
				maxValue = Math.max(maxValue, goal);
			}
			
			int size = values.size();
				for (int i = 0; i < size; i++) {
					value = (Value)values.elementAt(i);
					minValue = Math.min(minValue, value.getWeight());
					minValue = Math.min(minValue, value.getTrend());
					maxValue = Math.max(maxValue, value.getWeight());
					maxValue = Math.max(maxValue, value.getTrend());
				}
		}
		
		// Make the min and max values have a bit of breathing room in the chart
		minValue = (int)Math.floor(minValue - minMaxLabelPadding);
		maxValue += minMaxLabelPadding;
		if (maxValue - minValue < 1) {
			maxValue = minValue + 1;
		}
	}
	
	private void computePaddings() {
		if (UnitManager.getInstance(context).isStones()) {
			float width = yAxisTextPaint.measureText("0");
			if (maxValue / 14 >= 10) {
				width += yAxisTextPaint.measureText("0");
			}
			width += yAxisTextPaint.measureText("s0");
			if (
				(int)(maxValue / 14) != (int)(minValue / 14) ||
				maxValue % 14 >= 10 ||
				minValue % 14 >= 10
			) {
				width += yAxisTextPaint.measureText("0");
			}
			textWidth = (int) width;
		}
		else {
			textWidth = (int) yAxisTextPaint.measureText((int)maxValue + "");
		}
		textHeight = (int) -yAxisTextPaint.ascent();
		
		topPadding = (int) (textHeight);
		bottomPadding = (int) (height - textHeight - 2 * stringPadding);
		leftPadding = textWidth + 2 * stringPadding + 1;
		rightPadding = stringPadding;
	}
	
	private float xCoord(int days) {
		return (float) ((horizontalStep * (days + 1)) + leftPadding);
	}
	
	private float yCoord(float value) {
		return (float) ((verticalStep * (maxValue - value)) + topPadding);
	}
}
